package handler

import (
	"github.com/DiracLee/dires-go/app/cmdline"
	"github.com/DiracLee/dires-go/app/database"
	"github.com/DiracLee/dires-go/app/payload"
	"github.com/DiracLee/dires-go/utils"
	"strconv"
)

func init() {
	RegisterCommand(cmdline.CmdHSet, handleHSet, writeFirstKey, undoHSet, 4)
	RegisterCommand(cmdline.CmdHSetNX, handleHSetNX, writeFirstKey, undoHSet, 4)
	RegisterCommand(cmdline.CmdHMSet, handleHMSet, writeFirstKey, undoHMSet, -4)
	RegisterCommand(cmdline.CmdHGet, handleHGet, readFirstKey, nil, -3)
	RegisterCommand(cmdline.CmdHMGet, handleHMGet, readFirstKey, nil, -3)
	RegisterCommand(cmdline.CmdHExists, handleHExists, readFirstKey, nil, 3)
	RegisterCommand(cmdline.CmdHLen, handleHLen, readFirstKey, nil, 2)
	RegisterCommand(cmdline.CmdHDel, handleHDel, writeFirstKey, undoHDel, -3)
	RegisterCommand(cmdline.CmdHKeys, handleHKeys, readFirstKey, nil, 2)
	RegisterCommand(cmdline.CmdHVals, handleHVals, readFirstKey, nil, 2)
	RegisterCommand(cmdline.CmdHGetAll, handleHGetAll, readFirstKey, nil, 2)
	RegisterCommand(cmdline.CmdHIncrBy, handleHIncrBy, writeFirstKey, undoHIncr, 4)
	RegisterCommand(cmdline.CmdHIncrByFloat, handleHIncrByFloat, writeFirstKey, undoHIncr, 4)
}

func handleHSet(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	field := string(args[1])
	value := args[2]

	dict, _, errPayload := db.GetOrInitDict(key)
	if errPayload != nil {
		return errPayload
	}

	result := dict.PutOrSet(field, value)
	db.AddAOF(NamedCommand(cmdline.CmdHSet, args...))
	return payload.NewIntPayload(int64(result))
}

func handleHSetNX(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	field := string(args[1])
	value := args[2]

	dict, _, errPayload := db.GetOrInitDict(key)
	if errPayload != nil {
		return errPayload
	}

	result := dict.SetIfExists(field, value)
	db.AddAOF(NamedCommand(cmdline.CmdHSetNX, args...))
	return payload.NewIntPayload(int64(result))
}

func handleHMSet(db database.DB, args cmdline.CmdLine) payload.Payload {
	if len(args)%2 != 1 {
		return payload.NewSyntaxErrPayload()
	}
	key := string(args[0])
	size := (len(args) - 1) / 2
	fields := make([]string, size)
	values := make([][]byte, size)
	for i := 0; i < size; i++ {
		fields[i] = string(args[2*i+1])
		values[i] = args[2*i+2]
	}

	dict, _, errPayload := db.GetOrInitDict(key)
	if errPayload != nil {
		return errPayload
	}

	for i, field := range fields {
		value := values[i]
		dict.PutOrSet(field, value)
	}
	db.AddAOF(NamedCommand("hmset", args...))
	return &payload.OkPayload{}
}

func undoHSet(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	key := string(args[0])
	field := string(args[1])
	return rollbackHashFields(db, key, field)
}

func undoHMSet(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	key := string(args[0])
	size := (len(args) - 1) / 2
	fields := make([]string, size)
	for i := 0; i < size; i++ {
		fields[i] = string(args[2*i+1])
	}
	return rollbackHashFields(db, key, fields...)
}

func handleHGet(db database.DB, args cmdline.CmdLine) payload.Payload {
	// parse args
	key := string(args[0])
	field := string(args[1])

	// get entity
	dict, errPayload := db.GetAsDict(key)
	if errPayload != nil {
		return errPayload
	}
	if dict == nil {
		return &payload.NullBulkPayload{}
	}

	raw, exists := dict.Get(field)
	if !exists {
		return &payload.NullBulkPayload{}
	}
	value, _ := raw.([]byte)
	return payload.NewBulkPayload(value)
}

func handleHMGet(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	size := len(args) - 1
	fields := make([]string, size)
	for i := 0; i < size; i++ {
		fields[i] = string(args[i+1])
	}

	// get entity
	result := make([][]byte, size)
	dict, errPayload := db.GetAsDict(key)
	if errPayload != nil {
		return errPayload
	}
	if dict == nil {
		return payload.NewMultiBulkPayload(result)
	}

	for i, field := range fields {
		value, ok := dict.Get(field)
		if !ok {
			result[i] = nil
		} else {
			bytes, _ := value.([]byte)
			result[i] = bytes
		}
	}
	return payload.NewMultiBulkPayload(result)
}

func handleHExists(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	field := string(args[1])

	dict, errPayload := db.GetAsDict(key)
	if errPayload != nil {
		return errPayload
	}
	if dict == nil {
		return payload.NewIntPayload(0)
	}

	_, exists := dict.Get(field)
	if exists {
		return payload.NewIntPayload(1)
	}
	return payload.NewIntPayload(0)
}

func handleHLen(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])

	dict, errPayload := db.GetAsDict(key)
	if errPayload != nil {
		return errPayload
	}
	if dict == nil {
		return payload.NewIntPayload(0)
	}
	return payload.NewIntPayload(int64(dict.Len()))
}

func handleHDel(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	fields := make([]string, len(args)-1)
	fieldArgs := args[1:]
	for i, v := range fieldArgs {
		fields[i] = string(v)
	}

	// get entity
	dict, errPayload := db.GetAsDict(key)
	if errPayload != nil {
		return errPayload
	}
	if dict == nil {
		return payload.NewIntPayload(0)
	}

	deleted := 0
	for _, field := range fields {
		result := dict.Remove(field)
		deleted += result
	}
	if dict.Len() == 0 {
		db.Delete(key)
	}
	if deleted > 0 {
		db.AddAOF(NamedCommand("hdel", args...))
	}

	return payload.NewIntPayload(int64(deleted))
}

func undoHDel(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	key := string(args[0])
	fields := make([]string, len(args)-1)
	fieldArgs := args[1:]
	for i, v := range fieldArgs {
		fields[i] = string(v)
	}
	return rollbackHashFields(db, key, fields...)
}

func handleHKeys(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])

	dict, errPayload := db.GetAsDict(key)
	if errPayload != nil {
		return errPayload
	}
	if dict == nil {
		return &payload.EmptyMultiBulkPayload{}
	}

	fields := make([][]byte, dict.Len())
	i := 0
	dict.ForEach(func(key string, val interface{}) bool {
		fields[i] = []byte(key)
		i++
		return true
	})
	return payload.NewMultiBulkPayload(fields[:i])
}

func handleHVals(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])

	// get entity
	dict, errPayload := db.GetAsDict(key)
	if errPayload != nil {
		return errPayload
	}
	if dict == nil {
		return &payload.EmptyMultiBulkPayload{}
	}

	values := make([][]byte, dict.Len())
	i := 0
	dict.ForEach(func(key string, val interface{}) bool {
		values[i], _ = val.([]byte)
		i++
		return true
	})
	return payload.NewMultiBulkPayload(values[:i])
}

func handleHGetAll(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])

	// get entity
	dict, errPayload := db.GetAsDict(key)
	if errPayload != nil {
		return errPayload
	}
	if dict == nil {
		return &payload.EmptyMultiBulkPayload{}
	}

	size := dict.Len()
	result := make([][]byte, size*2)
	i := 0
	dict.ForEach(func(key string, val interface{}) bool {
		result[i] = []byte(key)
		i++
		result[i], _ = val.([]byte)
		i++
		return true
	})
	return payload.NewMultiBulkPayload(result[:i])
}

func handleHIncrBy(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	field := string(args[1])
	rawDelta := string(args[2])
	delta, err := strconv.ParseInt(rawDelta, 10, 64)
	if err != nil {
		return payload.NewErrPayload("ERR value is not an integer or out of range")
	}

	dict, _, errPayload := db.GetOrInitDict(key)
	if errPayload != nil {
		return errPayload
	}

	value, exists := dict.Get(field)
	if !exists {
		dict.PutOrSet(field, args[2])
		db.AddAOF(NamedCommand("hincrby", args...))
		return payload.NewBulkPayload(args[2])
	}
	val, err := strconv.ParseInt(string(value.([]byte)), 10, 64)
	if err != nil {
		return payload.NewErrPayload("ERR hash value is not an integer")
	}
	val += delta
	bytes := []byte(strconv.FormatInt(val, 10))
	dict.PutOrSet(field, bytes)
	db.AddAOF(NamedCommand("hincrby", args...))
	return payload.NewBulkPayload(bytes)
}

func handleHIncrByFloat(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	field := string(args[1])
	rawDelta := string(args[2])
	delta, err := utils.NewFromString(rawDelta)
	if err != nil {
		return payload.NewErrPayload("ERR value is not a valid float")
	}

	// get or init entity
	dict, _, errPayload := db.GetOrInitDict(key)
	if errPayload != nil {
		return errPayload
	}

	value, exists := dict.Get(field)
	if !exists {
		dict.PutOrSet(field, args[2])
		return payload.NewBulkPayload(args[2])
	}
	val, err := utils.NewFromString(string(value.([]byte)))
	if err != nil {
		return payload.NewErrPayload("ERR hash value is not a float")
	}
	result := val.Add(delta)
	resultBytes := []byte(result.String())
	dict.PutOrSet(field, resultBytes)
	db.AddAOF(NamedCommand("hincrbyfloat", args...))
	return payload.NewBulkPayload(resultBytes)
}

func undoHIncr(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	key := string(args[0])
	field := string(args[1])
	return rollbackHashFields(db, key, field)
}
